-
-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(testing): in-qemu tests #59
Conversation
with: | ||
command: dev-env | ||
- name: install qemu | ||
run: sudo apt-get install qemu |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i wonder if this would still fail if we changed it to
run: sudo apt-get install qemu | |
run: sudo apt-get update && sudo apt-get install qemu |
whoa it appears to "work" now: https://github.com/hawkw/mycelium/runs/373304967#step:6:236 |
Yup, seems like that did it, @hawkw. |
should we maybe have a separate build test for in-qemu tests vs crate unit tests? |
it looks like there's some code that needs to be cfg'd out when not building for the kernel target, or something: https://github.com/hawkw/mycelium/pull/59/checks?check_run_id=373309239 |
rebased onto master after merging #58 |
the clippy CI failure looks like an issue with the CI job rather than this branch, we should probably re-run it? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to make some changes to how we detect and report test failures before I'd feel okay about running this in CI. In particular, if a test fails by triggering a kernel oops (which currently happens on panics as well as on CPU exceptions), the test runner will hang forever instead of exiting with a test failure, which is not ideal.
I also commented on some minor nits that are not blockers, feel free to take them or leave them.
uses: actions-rs/[email protected] | ||
with: | ||
command: test | ||
command: test-x64 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like this only runs the in-qemu kernel tests, so if we add regular Rust unit tests in any of the lib crates, they're no longer being run on CI? IMHO we should probably have separate CI jobs for the in-qemu kernel tests and for regular rust unit tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think they were running on CI before anyway, as we were only running cargo test
on the root crate. We should definitely run tests for non-root crates though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think they were running on CI before anyway, as we were only running
cargo test
oh whoops, lol.
pub(crate) fn qemu_exit(exit_code: QemuExitCode) { | ||
let code = exit_code as u32; | ||
unsafe { | ||
asm!("out 0xf4, eax" :: "{eax}"(code) :: "intel","volatile"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, take it or leave it: i think that everywhere else, we use AT&T syntax for inline assembly. i'm fine with either, but i think we might want to agree on a project convention for consistency...since the operand ordering is reversed it can be kind of confusing IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(oh god does this mean we need to have a style guide)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can use the AT&T syntax, I'm just more used to intel syntax so tend to use it by default.
I don't think we need a style guide because of inline asm, but it's good to try to be generally consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i’m fine with either syntax (i actually have a slight preference for intel as well, for extremely silly reasons: [foo + bar]
is far more obvious to me than...however you say that in at&t, which i actually don’t even remember).
i think existing inline asm is at&t mostly just because most of it is about one instruction that doesn’t have any memory operands and i was too lazy to type :::: “intel”
. i would be absolutely fine with changing everything to intel. i’m sure @iximeow has at least one opinion about this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh another note is that since @iximeow is working on adding disassembly to kernel oopses using yaxpeax
, and i believe yax’s formatter is intel-ish, i have a slight preference for using the same syntax in the kernel source and in oopses
let span = tracing::info_span!("running test", test.name); | ||
let _enter = span.enter(); | ||
match (test.func)() { | ||
Ok(_) => tracing::info!(test.name, "TEST OK"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit/TIOLI: since test.name
is part of the parent span here, I am not convinced we need to include it in these messages as well. up to you.
match self { | ||
Ok(_) => Ok(()), | ||
Err(err) => { | ||
tracing::error!(?err, "test failed"); | ||
Err(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little unsure why we have this rather than just having the test cases return a fmt::Debug
trait object or something? but, i am not going to block this on that.
match self { | ||
Ok(_) => Ok(()), | ||
Err(err) => { | ||
tracing::error!(?err, "test failed"); | ||
Err(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, this is a nit, so take it or leave it, but can't this method just be
self.map_err(|err| tracing::error!(?err, "test failed"))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think it can
match wasm::run_wasm(HELLOWORLD_WASM) { | ||
Ok(()) => tracing::info!("wasm test Ok!"), | ||
Err(err) => tracing::error!(?err, "wasm test Err"), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: since we already log the output of the test case in the test harness wrapper, can't this just be
#[mycelium_util::test]
fn test_wasm() {
wasm::run_wasm(HELLOWORLD_WASM)
}
let span = tracing::info_span!("running test", test.name); | ||
let _enter = span.enter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, TIOLI: it seems like this is something the test-case macro could generate?
} | ||
|
||
#[cfg(test)] | ||
fn test_runner(tests: &[&mycelium_util::testing::TestCase]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
take it or leave it: I feel like a large chunk of this could go in the mycelium_util::testing
module and then we could just implement the qemu exit stuff here? not a blocker though.
#[test_case] | ||
const #test_case_ident: ::mycelium_util::testing::TestCase = ::mycelium_util::testing::TestCase { | ||
name: #test_name, | ||
func: || ::mycelium_util::testing::TestResult::into_result(#ident()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICT, the test cases always return ()
, and fail by panicking, but we don't capture panics because we're compiling with panic="abort"
. this implies to me that we should either A, figure out how to implement unwinding in the kernel (sounds hard + bad), or B, change the test case impls to return Result
or something and don't indicate failures by panicking, so that we can run the whole test suite and then report which tests failed, rather than panicking as soon as a single test case fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, C: we could change the way tests are run so that each individual test gets its own clean QEMU instance, and then we have a little binary on the build host that spawns those QEMUs and collates the test results. this might actually be the best choice, since it would allow tests to fail by panicking, and it would avoid the issue that the whole kernel is basically global mutable state, and would allow us to run each test in isolation without letting other tests runs (especially those that panic) to pollute the global kernel state...but, if we end up with a lot of in-qemu test cases, it might be much slower...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to make it such that our tests can return results, so we can recover from failure, rather than handling panicing assertions.
It'd be nice to have each test run in it's own clean QEMU instance, but I wasn't sure what the best way to do that was, given that we don't have a good mechanism to pass info in yet. One option would be to have the default behaviour just be to print out all available tests, and have the test runner then reset & re-call the kernel with each test in order.
It's also inconvenient with how it's currently written, as tests written outside of the mycelium_kernel
module don't share the same test runner, so we'd need to effectively write an entire boot routine for each integration test, etc. that we want to write.
I don't have a great answer here, unfortunately :'-(
@@ -43,6 +156,28 @@ fn panic(panic: &core::panic::PanicInfo) -> ! { | |||
arch::oops(&pp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we should add a line to arch::oops
like
#[cfg(test)]
qemu_exit(QemuExitCode::Failed);
since the current oops handler will display the oops screen and then hang so the user can read the oops screen. this means that if a test fails by panicking or by triggering a CPU fault, rather than exiting and reporting that the test failed, we will hang forever (which we especially don't want in CI). if we are going to fail tests using the Rust assert
macro, we definitely need to exit QEMU in the panic handler.
alternatively, we could put #[cfg(not(test))]
on the call to arch::oops
here, and have the panic handler exit qemu when running tests, but that means that CPU exceptions would still not result in normal test failures...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wanted to do this, but didn't in the first version, as your oops handler hadn't been written yet, and I didn't want to make my changes conflict :-)
As discussed in [#59 (comment)][1], it's good to standardize on a syntax, and @mystor and I both have a minor preference for Intel over AT&T syntax. Also, when the work started in PR #61 merges, we will be showing disassembly in crash screens using Intel syntax, so we should use consistent syntax in source. This PR changes inline assembly to use Intel syntax. In cases where the inline assembly is a single instruction with no syntactic differences between AT&T and Intel syntax (e.g. "cli", "hlt", et cetera), I did not add the "intel" flag to the `asm!` macro. I can if we'd prefer this to be explicit, but the syntax is currently equivalent. [1]: #59 (comment) Signed-off-by: Eliza Weisman <[email protected]>
* chore: change x86 inline asm from AT&T syntax to intel As discussed in [#59 (comment)][1], it's good to standardize on a syntax, and @mystor and I both have a minor preference for Intel over AT&T syntax. Also, when the work started in PR #61 merges, we will be showing disassembly in crash screens using Intel syntax, so we should use consistent syntax in source. This PR changes inline assembly to use Intel syntax. In cases where the inline assembly is a single instruction with no syntactic differences between AT&T and Intel syntax (e.g. "cli", "hlt", et cetera), I did not add the "intel" flag to the `asm!` macro. I can if we'd prefer this to be explicit, but the syntax is currently equivalent. [1]: #59 (comment) Signed-off-by: Eliza Weisman <[email protected]> * fixy fix Signed-off-by: Eliza Weisman <[email protected]>
Superceded by #65 |
A bit sketchy, but that's the nature of custom test harnesses like this.
Built on top of #58, and doesn't work on gh-actions yet.